iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0

上一篇介紹了 Projection Pushdown,讓資料來源盡量只讀取查詢需要的欄位。

今天介紹另一個重要的最佳化:

Filter Pushdown

它的核心概念是:

WHERE 條件盡可能往資料來源方向移動,越早排除不符合條件的資料,後續需要處理的資料就越少。


先回應讀者的問題:哪些資料來源支援 Filter Pushdown?

有讀者提問:

到底哪些資料來源支援 Pushdown?哪些不支援?

這不能只用「支援」或「不支援」二分法回答,因為實際能力取決於:

  • 資料來源的格式
  • TableProviderFileSource 的實作
  • 查詢條件是否能被資料來源理解
  • 是否有可用的統計資訊
  • 資料來源能否在讀取階段減少 I/O

可以先用下表理解常見情況:

資料來源 Filter Pushdown 情況 常見行為
Parquet 通常支援部分條件 利用 Row Group Statistics 跳過不可能符合的資料區塊
CSV 通常較有限 多半先解析資料,再由 FilterExec 篩選
JSON / NDJSON 通常較有限 需要先解析 JSON,較難直接跳過大量資料
MemoryTable 通常由 DataFusion 處理 資料已在記憶體中,主要減少下游運算量
外部資料庫或 API 取決於 TableProvider 實作 可將條件轉換成資料來源自己的查詢

因此,更精確的說法是:

DataFusion 會嘗試將 Filter 傳給資料來源,但資料來源是否能在讀取階段真正套用,取決於它支援的能力。


什麼是 Filter?

在 SQL 中,Filter 通常就是 WHERE 條件:

SELECT city, amount
FROM orders
WHERE amount > 1000;

這段查詢只保留:

amount > 1000

的資料列。

如果資料表有 100 萬筆資料,最直接的做法可能是:

讀取全部資料
    ↓
建立完整 RecordBatch
    ↓
套用 WHERE
    ↓
丟棄不符合條件的資料

這樣雖然結果正確,但可能已經花費大量成本讀取最後會被丟棄的資料。


沒有 Filter Pushdown 的情況

假設只有 1% 的資料符合條件:

資料來源:1,000,000 rows
        ↓
讀取全部資料
        ↓
FilterExec
        ↓
剩下 10,000 rows

也就是:

讀取:1,000,000 rows
保留:10,000 rows
丟棄:990,000 rows

這會增加:

  • 磁碟 I/O
  • 資料解碼成本
  • 記憶體使用量
  • Filter 運算量
  • 下游算子的資料傳遞量

使用 Filter Pushdown 的情況

如果資料來源支援 Filter Pushdown,流程可能變成:

資料來源:1,000,000 rows
        ↓
資料來源層級先套用條件
        ↓
讀取 10,000 rows
        ↓
交給後續執行算子

這樣可以避免讀取大量不需要的資料。


Filter Pushdown 不只是把 FilterExec 往前移

需要區分兩個概念:

FilterExec
→ DataFusion 執行階段的篩選算子

Filter Pushdown
→ 將條件交給更底層的資料來源處理

如果 Physical Plan 是:

DataSourceExec
    ↓
FilterExec

代表資料可能先被讀出來,再由 FilterExec 篩選。

真正的資料來源層級 Pushdown 則是:

Filter: amount > 1000
        ↓
TableProvider / FileSource
        ↓
資料來源先篩選或跳過資料

即使最後仍看到 FilterExec,也不代表完全沒有 Pushdown。資料來源可能先做部分篩選,再由 FilterExec 完成最後確認。


Parquet 如何支援 Filter Pushdown?

Parquet 通常會將資料分成多個 Row Group,並儲存欄位統計資訊。

例如:

Row Group 1
amount:
  min = 100
  max = 500

Row Group 2
amount:
  min = 800
  max = 2,000

查詢條件是:

WHERE amount > 1000;

DataFusion 可以判斷:

Row Group 1 的最大值只有 500,
不可能有 amount > 1000 的資料。

因此可以跳過:

Row Group 1 → 跳過
Row Group 2 → 讀取

這種根據統計資訊跳過資料區塊的方式,通常稱為:

Predicate Pruning

Parquet 的 Filter Pushdown 流程

SQL
  WHERE amount > 1000
        ↓
Logical Plan 的 Filter
        ↓
Parquet Data Source
        ↓
讀取 Row Group Statistics
        ↓
跳過不可能符合條件的 Row Group
        ↓
讀取剩餘資料

注意,Row Group Pruning 只能跳過「確定不可能符合」的資料區塊。

如果統計資訊是:

amount:
  min = 100
  max = 5,000

查詢條件是:

WHERE amount > 1000;

這個 Row Group 仍可能包含符合條件的資料,因此不能整個跳過。


CSV 為什麼通常比較有限?

CSV 是文字列格式:

city,amount,is_member
Taipei,1200.0,true
Taichung,850.0,false

CSV 通常沒有 Parquet 的 Row Group Statistics。讀取器需要先:

  1. 讀取文字內容
  2. 解析每一列
  3. 建立欄位值
  4. 判斷 WHERE 條件

流程比較接近:

CSV
  ↓
解析資料列
  ↓
建立 RecordBatch
  ↓
FilterExec
  ↓
保留符合條件的資料

所以 CSV 即使能在計畫層知道有篩選條件,也不一定能在檔案讀取階段跳過大量資料。


MemoryTable 的情況

如果資料已經放在記憶體中,例如 MemTable

MemoryExec
    ↓
FilterExec

此時資料已經存在 Arrow RecordBatch 中,Pushdown 的重點不是減少磁碟 I/O,而是:

  • 減少下游算子收到的資料量
  • 降低聚合與排序成本
  • 減少資料在執行計畫中的傳遞量

外部資料庫與自訂 TableProvider

DataFusion 可以透過自訂 TableProvider 連接外部資料來源:

PostgreSQL
MySQL
REST API
遠端資料服務

如果 TableProvider 能理解查詢條件,就可以將:

WHERE amount > 1000

轉換成資料來源自己的查詢。

DataFusion Filter
        ↓
TableProvider
        ↓
外部資料庫先篩選
        ↓
只回傳符合條件的資料

在 Rust 的自訂 TableProvider 中,可以透過 supports_filters_pushdown 回報每個條件的支援程度:

Exact
→ 資料來源完整處理條件

Inexact
→ 資料來源可以減少資料,但仍可能需要 FilterExec

Unsupported
→ 資料來源不處理,由 DataFusion 篩選

這讓 DataFusion 知道哪些條件可以安全地下推。


Filter Pushdown 與 Projection Pushdown

實際查詢通常會同時使用兩種 Pushdown:

SELECT city, amount
FROM orders
WHERE is_member = true
  AND amount > 1000;

這段查詢需要的欄位是:

city
amount
is_member

可能的計畫如下:

Projection: city, amount
  Filter: is_member = true AND amount > 1000
    TableScan: orders
      projection=[city, amount, is_member]

兩者分工如下:

Projection Pushdown
→ 減少欄位數量

Filter Pushdown
→ 減少資料列數量

is_member 雖然不會出現在輸出結果中,但因為它被 WHERE 使用,所以仍然必須讀取。


使用 EXPLAIN 觀察 Filter

在 DataFusion CLI 執行:

EXPLAIN FORMAT INDENT
SELECT city, amount
FROM orders
WHERE is_member = true
  AND amount > 1000;

可能會看到:

logical_plan
  Projection: orders.city, orders.amount
    Filter: orders.is_member = Boolean(true)
            AND orders.amount > Int64(1000)
      TableScan: orders

Physical Plan 可能接近:

physical_plan
  ProjectionExec
    FilterExec
      DataSourceExec

不同資料來源與 DataFusion 版本,輸出格式可能不同。


如何確認 Pushdown 是否真的有效?

可以使用:

EXPLAIN ANALYZE
SELECT city, amount
FROM orders
WHERE is_member = true
  AND amount > 1000;

觀察:

  • DataSource 實際讀取的資料量
  • FilterExec 的輸入與輸出資料量
  • RecordBatch 數量
  • Parquet 是否跳過 Row Group
  • 查詢執行時間

如果資料來源仍讀取完整資料,再由 FilterExec 篩選,通常表示條件沒有在資料來源層級完全生效。


今日小結

今天透過讀者的問題,釐清了 Filter Pushdown 的支援情況:

  • Parquet 通常可以利用統計資訊進行 Row Group Pruning
  • CSV 通常需要先解析資料,再由 FilterExec 篩選
  • JSON / NDJSON 的支援程度取決於讀取器實作
  • MemoryTable 主要減少下游資料處理量
  • 外部資料庫或 API 可以透過自訂 TableProvider 實作 Pushdown
  • Filter Pushdown 有 ExactInexactUnsupported 三種結果
  • 看到 FilterExec 不代表一定沒有發生 Pushdown
  • EXPLAIN ANALYZE 可以協助觀察實際執行效果

可以用一句話總結:

Filter Pushdown 讓資料在越靠近來源的地方越早被過濾,減少不必要的資料讀取與後續運算;但是否能真正降低 I/O,取決於資料來源的實作能力。

下一篇將介紹 Parquet Metadata、Statistics 與 Predicate Pruning,進一步了解資料來源如何根據統計資訊跳過不必要的 Row Group。

延伸閱讀


上一篇
Day 17|Projection Pushdown:只讀真正需要的欄位
系列文
深入 SQL 查詢引擎:30 天用 Rust 與 Apache DataFusion 解構資料處理流程18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言